package com.eunion.core.common.jpa.impl;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;

import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaDelete;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Selection;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;

import com.eunion.core.common.jpa.OrderUtil;
import com.eunion.core.common.jpa.ProjectionQueryExecutor;
import com.eunion.core.common.jpa.deletgate.ProjectionJpaEntityInformation;
import com.eunion.core.common.jpa.query.BasicDelete;
import com.eunion.core.common.jpa.query.QueryRequirement;

public class SimpleProjectionQueryExecutorImpl<Entity, Bean, ID extends Serializable>
        extends SimpleJpaRepository<Entity, ID> implements ProjectionQueryExecutor<Entity, Bean> {

    private final ProjectionJpaEntityInformation<Entity, Bean, Serializable> entityInformation;

    private final EntityManager em;

    @SuppressWarnings({ "unchecked", "rawtypes" })
    public SimpleProjectionQueryExecutorImpl(JpaEntityInformation<Entity, ID> entityInformation,
            EntityManager entityManager) {
        super(entityInformation, entityManager);
        this.em = entityManager;
        if (entityInformation instanceof ProjectionJpaEntityInformation) {
            this.entityInformation = (ProjectionJpaEntityInformation<Entity, Bean, Serializable>) entityInformation;
        } else {
            this.entityInformation = new ProjectionJpaEntityInformation(entityInformation);
        }
    }

    @Override
    public Entity load(Serializable id) {
        Class<Entity> domainType = getDomainClass();
        return em.getReference(domainType, id);
    }

    @Override
    public Optional<Bean> findOne(QueryRequirement<Entity, Bean> req) {
        Bean singleResult;
        try {
            singleResult = generateQuery(req, (Pageable) null).getSingleResult();
        } catch (NoResultException e) {
            singleResult = null;
        }
        return Optional.ofNullable(singleResult);
    }

    @Override
    public List<Bean> findAll(QueryRequirement<Entity, Bean> req) {
        return generateQuery(req, (Pageable) null).getResultList();
    }

    @Override
    public List<Bean> findAll(QueryRequirement<Entity, Bean> req, Sort sort) {
        return generateQuery(req, sort).getResultList();
    }

    @Override
    public Page<Bean> findAll(QueryRequirement<Entity, Bean> req, Pageable pageable) {
        return readPage(generateQuery(req, pageable), pageable, req);
    }

    protected TypedQuery<Bean> generateQuery(QueryRequirement<Entity, Bean> req, Pageable pageable) {
        Sort sort = pageable == null ? null : pageable.getSort();
        return generateQuery(req, sort);
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    protected TypedQuery<Bean> generateQuery(QueryRequirement<Entity, Bean> req, Sort sort) {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Bean> query = builder.createQuery(entityInformation.getBeanClass());

        Root<Entity> root = applyCriteria(req, query);
        List<Selection<?>> selections = new ArrayList<>(req.selections(root, query, builder));
        if (!ObjectUtils.isEmpty(selections)) {
            query.multiselect(selections);
        } else {
            query.select((Root) root);
        }
        List<Expression<?>> groupBy = req.groupBy(root);
        if (!ObjectUtils.isEmpty(groupBy)) {
            query.groupBy(groupBy);
        }
        if (sort != null) {
            query.orderBy(OrderUtil.toOrders(sort, root, builder));
        }

        return em.createQuery(query);
    }

    private Page<Bean> readPage(TypedQuery<Bean> query, Pageable pageable, final QueryRequirement<Entity, Bean> req) {
        // TODO commons升级后切回getOffset()
        Number offset = pageable.getPageNumber() * pageable.getPageSize();
        query.setFirstResult(offset.intValue());
        query.setMaxResults(pageable.getPageSize());
        return getPage(query.getResultList(), pageable,
                () -> executeCountQuery(generateCountQuery(req)));
    }

    public static <T> Page<T> getPage(List<T> content, Pageable pageable, Supplier<Long> totalSupplier) {
        Assert.notNull(content, "Content must not be null!");
        Assert.notNull(totalSupplier, "TotalSupplier must not be null!");
        // TODO commons升级后切回getOffset()
        if (pageable == null || pageable.getPageSize() == 0 || pageable.getPageNumber() == 0) {
            if (pageable == null || pageable.getPageSize() > content.size()) {
                return new SimplePage<T>(content, pageable, content.size());
            }
            return new SimplePage<T>(content, pageable, totalSupplier.get());
        }
        if (content.size() != 0 && pageable.getPageSize() > content.size()) {
            return new SimplePage<T>(content, pageable, pageable.getOffset() + content.size());
        }
        return new SimplePage<T>(content, pageable, totalSupplier.get());
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    private TypedQuery<Long> generateCountQuery(QueryRequirement req) {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Long> query = builder.createQuery(Long.class);
        Root<Entity> root = query.from(entityInformation.getJavaType());
        if (req != null) {
            Predicate predicate = req.where(root, query, builder);
            if (predicate != null) {
                query.where(predicate);
            }
        }
        List<Expression<?>> selections = req.selections(root, query, builder);
        if (query.isDistinct()) {
            if (!ObjectUtils.isEmpty(selections)) {
                query.select(builder
                        .countDistinct(selections.stream().findFirst().orElseThrow(IllegalArgumentException::new)));
            } else {
                query.select(builder.countDistinct(root));
            }

        } else {
            if (!ObjectUtils.isEmpty(selections)) {
                query.select(builder.count(selections.stream().findFirst().orElseThrow(IllegalArgumentException::new)));
            } else {
                query.select(builder.count(root));
            }
        }
        // 将req中设置order by语句置空
        query.orderBy(Collections.emptyList());
        return em.createQuery(query);
    }

    private Long executeCountQuery(TypedQuery<Long> query) {
        Assert.notNull(query, "TypedQuery must not be null!");
        List<Long> totals = query.getResultList();
        Long total = 0L;
        for (Long element : totals) {
            total += element == null ? 0 : element;
        }
        return total;
    }

    private Root<Entity> applyCriteria(QueryRequirement<Entity, Bean> req, CriteriaQuery<Bean> query) {
        Root<Entity> root = query.from(entityInformation.getJavaType());
        if (req == null) {
            return root;
        }
        req.applyFetches(root);
        CriteriaBuilder builder = em.getCriteriaBuilder();
        Predicate predicate = req.where(root, query, builder);
        if (predicate != null) {
            query.where(predicate);
        }
        return root;
    }

    @Override
    public Long batchDelete(BasicDelete<Entity> req) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaDelete<Entity> delete = cb.createCriteriaDelete(entityInformation.getJavaType());
        Root<Entity> root = delete.from(entityInformation.getJavaType());
        Predicate restrictions = req.where(root, delete, cb);
        if (restrictions != null) {
            delete.where(restrictions);
        }
        return (long) em.createQuery(delete).executeUpdate();
    }

}
